const ethUtil = require('ethereumjs-util');
const abi = require('ethereumjs-abi');
const chai = require('chai');
const { expect } = require('chai');
require('dotenv').config();

const typedData = {
  types: {
    EIP712Domain: [
      { name: 'name', type: 'string' },
      { name: 'version', type: 'string' },
      { name: 'chainId', type: 'uint256' },
      { name: 'verifyingContract', type: 'address' },
    ],
    Person: [
      { name: 'name', type: 'string' },
      { name: 'wallet', type: 'address' },
    ],
    Mail: [
      { name: 'from', type: 'Person' },
      { name: 'to', type: 'Person' },
      { name: 'contents', type: 'string' },
    ],
  },
  primaryType: 'Mail',
  domain: {
    name: 'Ether Mail',
    version: '1',
    chainId: 1,
    verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
  },
  message: {
    from: {
      name: 'Cow',
      wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
    },
    to: {
      name: 'Bob',
      wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
    },
    contents: 'Hello, Bob!',
  },
};

const types = typedData.types;

// Recursively finds all the dependencies of a type
function dependencies(primaryType, found = []) {
  if (found.includes(primaryType)) {
    return found;
  }
  if (types[primaryType] === undefined) {
    return found;
  }
  found.push(primaryType);
  for (let field of types[primaryType]) {
    for (let dep of dependencies(field.type, found)) {
      if (!found.includes(dep)) {
        found.push(dep);
      }
    }
  }
  return found;
}

function encodeType(primaryType) {
  // Get dependencies primary first, then alphabetical
  let deps = dependencies(primaryType);
  deps = deps.filter((t) => t != primaryType);
  deps = [primaryType].concat(deps.sort());

  // Format as a string with fields
  let result = '';
  for (let type of deps) {
    result += `${type}(${types[type]
      .map(({ name, type }) => `${type} ${name}`)
      .join(',')})`;
  }
  return result;
}

function typeHash(primaryType) {
  return ethUtil.keccak256(encodeType(primaryType));
}

function encodeData(primaryType, data) {
  let encTypes = [];
  let encValues = [];

  // Add typehash
  encTypes.push('bytes32');
  encValues.push(typeHash(primaryType));

  // Add field contents
  for (let field of types[primaryType]) {
    let value = data[field.name];
    if (field.type == 'string' || field.type == 'bytes') {
      encTypes.push('bytes32');
      value = ethUtil.keccak256(value);
      encValues.push(value);
    } else if (types[field.type] !== undefined) {
      encTypes.push('bytes32');
      value = ethUtil.keccak256(encodeData(field.type, value));
      encValues.push(value);
    } else if (field.type.lastIndexOf(']') === field.type.length - 1) {
      throw 'TODO: Arrays currently unimplemented in encodeData';
    } else {
      encTypes.push(field.type);
      encValues.push(value);
    }
  }

  return abi.rawEncode(encTypes, encValues);
}

function structHash(primaryType, data) {
  return ethUtil.keccak256(encodeData(primaryType, data));
}

function signHash() {
  return ethUtil.keccak256(
    Buffer.concat([
      Buffer.from('1901', 'hex'),
      structHash('EIP712Domain', typedData.domain),
      structHash(typedData.primaryType, typedData.message),
    ])
  );
}

describe('EIP712 compatible logic', async () => {
  it('Test the function of EIP712', async () => {
    // Get test privatekey
    const privateKey = ethUtil.keccak256('cow');
    const targetAddress = ethUtil.privateToAddress(privateKey).toString('hex');
    const sig = ethUtil.ecsign(signHash(), privateKey);

    const eip712Factory = await ethers.getContractFactory('EIP712Test');

    const eip712Contract = await eip712Factory.deploy();

    // Set info of from
    await eip712Contract.setFrom(
      targetAddress,
      typedData.message.from.name,
      typedData.message.from.wallet
    );

    // Set info of to
    await eip712Contract.setTo(
      targetAddress,
      typedData.message.to.name,
      typedData.message.to.wallet
    );

    // Set content
    await eip712Contract.setContent(targetAddress, typedData.message.contents);

    // Get ecrecover address
    expect(
      await eip712Contract.verify(
        targetAddress,
        sig.v,
        ethUtil.bufferToHex(sig.r),
        ethUtil.bufferToHex(sig.s)
      )
    ).to.be.true;

    /* const privateKey = ethUtil.keccak256('cow');
        const address = ethUtil.privateToAddress(privateKey);
        const sig = ethUtil.ecsign(signHash(), privateKey);

        const expect = chai.expect;
        expect(encodeType('Mail')).to.equal('Mail(Person from,Person to,string contents)Person(string name,address wallet)');
        expect(ethUtil.bufferToHex(typeHash('Mail'))).to.equal(
            '0xa0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2',
        );
        expect(ethUtil.bufferToHex(encodeData(typedData.primaryType, typedData.message))).to.equal(
            '0xa0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2fc71e5fa27ff56c350aa531bc129ebdf613b772b6604664f5d8dbe21b85eb0c8cd54f074a4af31b4411ff6a60c9719dbd559c221c8ac3492d9d872b041d703d1b5aadf3154a261abdd9086fc627b61efca26ae5702701d05cd2305f7c52a2fc8',
        );
        expect(ethUtil.bufferToHex(structHash(typedData.primaryType, typedData.message))).to.equal(
            '0xc52c0ee5d84264471806290a3f2c4cecfc5490626bf912d01f240d7a274b371e',
        );
        expect(ethUtil.bufferToHex(structHash('EIP712Domain', typedData.domain))).to.equal(
            '0xf2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f',
        );
        expect(ethUtil.bufferToHex(signHash())).to.equal('0xbe609aee343fb3c4b28e1df9e632fca64fcfaede20f02e86244efddf30957bd2');
        expect(ethUtil.bufferToHex(address)).to.equal('0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826');
        expect(sig.v).to.equal(28);
        expect(ethUtil.bufferToHex(sig.r)).to.equal('0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d');
        expect(ethUtil.bufferToHex(sig.s)).to.equal('0x07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b91562');

        console.log(process.env.PRIVATE_KEY); */
  });
});
